A use-after-free vulnerability occurs when we are allowed to write to an already freed chunk as if it were still a valid allocation. The next time malloc
is invoked with that particular chunk size, a pointer to the same memory where the previously freed chunk was will be returned. This means that the now in-use chunk actually has the malicious data we put into that memory.
Such vulnerabilities occur when a pointer to heap memory is freed, but that pointer is still used afterwards.
Writing to the free chunk also allows for messing with the linked lists pointers. Overwriting the fwd
pointer with a value that points outside the heap can result in the modification of arbitrary memory.
use_after_free_logic.c:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct User
{
char Username[32];
int IsLoggedIn;
};
struct Service
{
char Name[32];
int IsEnabled;
};
int main(void)
{
char input[128];
struct User* user = NULL;
struct Service* srv = NULL;
while(1)
{
printf("\nType 'register [username]' in order to create a new user.\n");
printf("Type 'login' to login as a user. \n");
printf("Type 'service [name]' to create a new service. \n");
printf("Type 'logout' to log out of the current user. \n");
if(fgets(input, sizeof(input), stdin) == NULL) break;
if(strncmp(input, "register ", 9) == 0)
{
user = malloc(sizeof(struct User));
if(strlen(input + 9) < 31)
{
strcpy(user->Username, input + 9);
user->IsLoggedIn = 0;
}
}
if(strncmp(input, "login", 5) == 0)
{
printf("Login successful. \n");
user->IsLoggedIn = 1;
}
if(strncmp(input, "logout", 6) == 0)
{
free(user);
}
if(strncmp(input, "service ", 8) == 0)
{
srv = malloc(sizeof(struct Service));
if(strlen(input + 8) < 31)
{
strcpy(srv->Name, input + 8);
}
printf("Executing service...\n");
if(srv == NULL)
{
printf("Error: Service does not exists.\n");
}
else if(srv->IsEnabled)
{
printf("Service successfully executed.\n");
break;
}
else
{
printf("Error: Service is not enabled.\n");
}
}
}
return 0;
}
Our goal is to successfully get to the "Service successfully executed."
message.
Looking at the above code, we see that there are two structs - User
and Service
- with essentially the same memory layout. This programme appears to be some sort of a simple user/service manager. At first glance, we can register a new user with a given username, log into that user, log out of that user and create and attempt to run a service.
Let's see what happens, if we run the program as intended:
We witness an error telling us that the service has not been enabled. Hmm, let's take a closer look at the Service
struct. It is comprised of a 32 character name and a flag telling us whether or not the service has been enabled. Furthermore, we notice that the User
struct has essentially the same memory layout. Now, this could serve as an attack surface if we manage to force the program to allocate two of those structs - the User
and the Service
- in the same memory space.
When a heap chunk is freed, if a new chunk of the exactly same size is requested in a reasonable time-frame, malloc
will return a pointer to that original freed chunk. Most of the data that was present in this chunk will then still remain intact and could corrupt the new chunk.
What we need is to set the IsEnabled
member of the service to 1. Putting the code under scrutiny, we realise that we can freely control the IsLoggedIn
member of the user through the login
command. Furthermore, we can actually delete a user by invoking logout
. Hmm... the Service
and User
structs have the same size... I wonder what would happen if I were to create a service right after I have logged out of a user. Well, let's find out!
Well, well, well, would you look at that! The service was successfully executed. But what happened?
We first created a user with register
. Upon logging into the user with login
, the IsLoggedIn
member was set to 1. With logout
, the user was deleted and the chunk on the heap was freed. However, the chunk data isn't completely overwritten (only the fwd
and bwd
pointers are). Consequently, the location where IsLoggedIn
was stored on the heap still contains a 1. When we call service
, memory was requested from the heap. Since the size was the same as the size before, malloc
returned the chunk where the User
struct was previously stored. Ergo, the IsEnabled
member is actually stored at the same memory where IsLoggedIn
was. However, we already put a 1 into that memory with login
. Ergo, IsEnabled
is set to 1 and the service is executed.